package com.gitee.wsl.data.input.viewmodel

import com.gitee.wsl.data.input.SideJobScope
import com.gitee.wsl.data.input.interceptor.BallastInterceptor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take


/**
 * Notifications sent to [BallastInterceptor] to inspect the internal state of the ViewModel.
 */
 sealed class InputNotification<Inputs : Any, Events : Any, State : Any>(
     val viewModelType: String,
     val viewModelName: String,
) {

// ViewModel
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * The ViewModel was created and has started internal processing.
     */
     class ViewModelStatusChanged<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val status: Status,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "ViewModel status moved to: $status"
        }
    }

// Inputs
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * An Input was successfully send into the input channel, but has not been filtered checked through the filter yet.
     * If the Input was queued, the input channel was not full.
     */
     class InputQueued<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Input Queued: $input"
        }
    }

    /**
     * An Input was successfully queued in the ViewModel's Inputs channel and passed its Filter, so will be accepted
     * and passed to the Handler for processing.
     */
     class InputAccepted<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Accepting input: $input"
        }
    }

    /**
     * An Input was successfully queued in the ViewModel's Inputs channel but failed to pass its Filter.
     */
     class InputRejected<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val stateWhenRejected: State,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Rejecting input: $input"
        }
    }

    /**
     * The ViewModel was cleared or its Inputs channel's buffer was full and the Input could not even be queued to be
     * processed.
     */
     class InputDropped<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Dropping input: $input"
        }
    }

    /**
     * An Input was cancelled during processing, either due to a new input coming in while it was
     * still processing, or because the ViewModel went out of scope.
     */
     class InputHandledSuccessfully<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Input handled successfully: $input"
        }
    }

    /**
     * An Input was cancelled during processing, either due to a new input coming in while it was
     * still processing, or because the ViewModel went out of scope.
     */
     class InputCancelled<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Input cancelled: $input"
        }
    }

    /**
     * A normal exception was thrown during handling of an Input. Inputs should typically handle errors in other ways,
     * but it is not considered a fatal issue that should terminate the app, since it was able to be caught by the
     * ViewModel.
     */
     class InputHandlerError<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val input: Inputs,
         val throwable: Throwable,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Error handling input: $input (${throwable.message})"
        }
    }

// Events
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * An Event was posted to the queue to eventually be delivered to the EventHandler.
     */
     class EventQueued<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val event: Events,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Event Queued: $event"
        }
    }

    /**
     * An Event was posted to the EventHandler.
     */
     class EventEmitted<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val event: Events,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Emitting event: $event"
        }
    }

    /**
     * An Event was posted to the EventHandler.
     */
     class EventHandledSuccessfully<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val event: Events,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Event handled successfully: $event"
        }
    }

    /**
     * A normal exception was thrown during disptching of an Event. Events should typically handle errors in other ways,
     * but it is not considered a fatal issue that should terminate the app, since it was able to be caught by the
     * ViewModel.
     */
     class EventHandlerError<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val event: Events,
         val throwable: Throwable,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Error handling event: $event (${throwable.message})"
        }
    }

    /**
     * The UI EventHandler has become in a valid Lifecycle state and has started processing Events
     */
     class EventProcessingStarted<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Event processing started"
        }
    }

    /**
     * The UI EventHandler has become in an invalid Lifecycle state and has stopped processing Events
     */
     class EventProcessingStopped<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Event processing stopped"
        }
    }

// States
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * A new State was emitted to the UI.
     */
     class StateChanged<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val state: State,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "State changed: $state"
        }
    }

// Side Jobs
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * A sideJob was queued, but has not started yet
     */
     class SideJobQueued<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val key: String,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "sideJob queued: $key"
        }
    }

    /**
     * A sideJob was started or restarted
     */
     class SideJobStarted<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val key: String,
         val restartState: SideJobScope.RestartState,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return when (restartState) {
                SideJobScope.RestartState.Initial -> "sideJob started: $key"
                SideJobScope.RestartState.Restarted -> "sideJob restarted: $key"
            }
        }
    }

    /**
     * A sideJob ran to completion
     */
     class SideJobCompleted<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val key: String,
         val restartState: SideJobScope.RestartState,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "sideJob finished: $key"
        }
    }

    /**
     * A sideJob was cancelled
     */
     class SideJobCancelled<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val key: String,
         val restartState: SideJobScope.RestartState,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "sideJob cancelled: $key"
        }
    }

    /**
     * A exception was thrown inside a sideJob
     */
     class SideJobError<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val key: String,
         val restartState: SideJobScope.RestartState,
         val throwable: Throwable,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Error in sideJob: $key (${throwable.message})"
        }
    }

// Interceptors
// ---------------------------------------------------------------------------------------------------------------------

     class InterceptorAttached<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val interceptor: BallastInterceptor<Inputs, Events, State>,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Interceptor attached: $interceptor"
        }
    }

     class InterceptorFailed<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val interceptor: BallastInterceptor<Inputs, Events, State>,
         val throwable: Throwable,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Interceptor failed: $interceptor (${throwable.message})"
        }
    }

// Other
// ---------------------------------------------------------------------------------------------------------------------

    /**
     * An exception was thrown somewhere within the ViewModel, but we're not quite sure from where. This is usually a
     * serious concern, and likely a candidate to terminate the app (if it didn't terminate itself already).
     */
     class UnhandledError<Inputs : Any, Events : Any, State : Any>(
        viewModelType: String,
        viewModelName: String,
         val throwable: Throwable,
    ) : InputNotification<Inputs, Events, State>(viewModelType, viewModelName) {
        override fun toString(): String {
            return "Uncaught error (${throwable.message})"
        }
    }
}


/**
 * Used for keeping track of the state of discrete "subjects" within an Interceptor. For example, a single Input will
 * send Notifications for [InputNotification.InputQueued], [InputNotification.InputAccepted], and
 * [InputNotification.InputHandledSuccessfully] during it's full processing journey, but the `input` property of all
 * 3 events will be the same instance, and should be associated to the same value.
 *
 * This method will associate values for each Notification's subject (the Input, Event, or SideJob key) and store them
 * in the corresponding [cache] map. When [removeValue], values will be removed from the cache when they are completed.
 * Otherwise, you can add a default value to remain in the cache so that it can be read later to give a discrete signal
 * that it has finished processing.
 */
 fun <Inputs : Any, Events : Any, State : Any, T : Any> InputNotification<Inputs, Events, State>.associate(
    cache: MutableMap<Any, T>,
    computeValueForSubject: (Any) -> T,
    onValueRemoved: (Any, T) -> Unit = { _, _ -> },

    removeValue: Boolean = true,
): T {
    val addValueToCache: (Any) -> T = {
        cache.getOrPut(it) { computeValueForSubject(it) }
    }
    val removeValueFromCache: (Any) -> T = { subject ->
        if (removeValue) {
            cache.remove(subject)
                ?.also { deferred -> onValueRemoved(subject, deferred) }
                ?: computeValueForSubject(subject).also { deferred -> onValueRemoved(subject, deferred) }
        } else {
            cache.getOrPut(subject) {
                computeValueForSubject(subject)
                    .also { deferred -> onValueRemoved(subject, deferred) }
            }
        }
    }

    return when (this) {
        is InputNotification.InputQueued -> {
            addValueToCache(this.input)
        }

        is InputNotification.InputAccepted -> {
            addValueToCache(this.input)
        }

        is InputNotification.InputRejected -> {
            removeValueFromCache(this.input)
        }

        is InputNotification.InputDropped -> {
            removeValueFromCache(this.input)
        }

        is InputNotification.InputHandledSuccessfully -> {
            removeValueFromCache(this.input)
        }

        is InputNotification.InputCancelled -> {
            removeValueFromCache(this.input)
        }

        is InputNotification.InputHandlerError -> {
            removeValueFromCache(this.input)
        }

        is InputNotification.EventQueued -> {
            addValueToCache(this.event)
        }

        is InputNotification.EventEmitted -> {
            addValueToCache(this.event)
        }

        is InputNotification.EventHandledSuccessfully -> {
            removeValueFromCache(this.event)
        }

        is InputNotification.EventHandlerError -> {
            removeValueFromCache(this.event)
        }

        is InputNotification.SideJobQueued -> {
            addValueToCache(this.key)
        }

        is InputNotification.SideJobStarted -> {
            addValueToCache(this.key)
        }

        is InputNotification.SideJobCompleted -> {
            removeValueFromCache(this.key)
        }

        is InputNotification.SideJobCancelled -> {
            removeValueFromCache(this.key)
        }

        is InputNotification.SideJobError -> {
            removeValueFromCache(this.key)
        }

        is InputNotification.StateChanged -> {
            // since states are always emitted only once and do not have status changed over time, we compute the
            // value directly and never cache it
            computeValueForSubject(this.state)
        }

        is InputNotification.EventProcessingStarted -> {
            addValueToCache(viewModelName)
        }

        is InputNotification.EventProcessingStopped -> {
            addValueToCache(viewModelName)
        }

        is InputNotification.UnhandledError -> {
            addValueToCache(viewModelName)
        }

        is InputNotification.ViewModelStatusChanged -> {
            if (this.status == Status.Cleared) {
                removeValueFromCache(viewModelName)
            } else {
                addValueToCache(viewModelName)
            }
        }

        is InputNotification.InterceptorAttached -> {
            addValueToCache(interceptor)
        }

        is InputNotification.InterceptorFailed -> {
            removeValueFromCache(interceptor)
        }
    }
}

// Helpers for collecting values in interceptors
// ---------------------------------------------------------------------------------------------------------------------

/**
 * Suspend until the ViewModel has started
 */
 suspend inline fun <Inputs : Any, Events : Any, State : Any> Flow<InputNotification<Inputs, Events, State>>.awaitViewModelStart() {
    filter {
        it is InputNotification.ViewModelStatusChanged<Inputs, Events, State> && it.status == Status.Running
    }.take(1)
        .collect()
}

/**
 * Return a `Flow` of Inputs that have been sent to the ViewModel. There is no assumption that any of these Inputs pass
 * the [InputFilter] or have completed successfully. This should be used for synchronization or reporting on the status
 * of Inputs, rather than performing some action as a result of an Input.
 *
 * The flow of Inputs can be buffered with [bufferInputs], which can be used to debounce, sample, etc. This buffering
 * should generally be controlled by the end-user, so it should be passed into the Interceptor and forwarded to this
 * function.
 */
inline fun <Inputs : Any, Events : Any, State : Any> Flow<InputNotification<Inputs, Events, State>>.queuedInputs(
    bufferInputs: (Flow<Inputs>) -> Flow<Inputs>
): Flow<Inputs> {
    return filterIsInstance<InputNotification.InputQueued<Inputs, Events, State>>()
        .map { it.input }
        .let { bufferInputs(it) }
}

/**
 * Return a `Flow` of Inputs that have been sent to the ViewModel and accepted for processing.
 *
 * The flow of Inputs can be buffered with [bufferInputs], which can be used to debounce, sample, etc. This buffering
 * should generally be controlled by the end-user, so it should be passed into the Interceptor and forwarded to this
 * function.
 */
 inline fun <Inputs : Any, Events : Any, State : Any> Flow<InputNotification<Inputs, Events, State>>.inputs(
    bufferInputs: (Flow<Inputs>) -> Flow<Inputs>
): Flow<Inputs> {
    return filterIsInstance<InputNotification.InputAccepted<Inputs, Events, State>>()
        .map { it.input }
        .let { bufferInputs(it) }
}

/**
 * Return a `Flow` of Events that have been emitted by the processing of an Input.
 *
 * The flow of Events can be buffered with [bufferEvents], which can be used to debounce, sample, etc. This buffering
 * should generally be controlled by the end-user, so it should be passed into the Interceptor and forwarded to this
 * function.
 */
 inline fun <Inputs : Any, Events : Any, State : Any> Flow<InputNotification<Inputs, Events, State>>.events(
    bufferEvents: (Flow<Events>) -> Flow<Events>
): Flow<Events> {
    return filterIsInstance<InputNotification.EventEmitted<Inputs, Events, State>>()
        .map { it.event }
        .let { bufferEvents(it) }
}

/**
 * Return a `Flow` of the States updated by the VIewModel
 *
 * The flow of Events can be buffered with [bufferState], which can be used to debounce, sample, etc. This buffering
 * should generally be controlled by the end-user, so it should be passed into the Interceptor and forwarded to this
 * function.
 */
inline fun <Inputs : Any, Events : Any, State : Any> Flow<InputNotification<Inputs, Events, State>>.states(
    bufferStates: (Flow<State>) -> Flow<State>
): Flow<State> {
    return filterIsInstance<InputNotification.StateChanged<Inputs, Events, State>>()
        .map { it.state }
        .let { bufferStates(it) }
}
